/**
 * background.js
 * @fileOverview Contains the javascript code that runs in the background page of the extension.
 * @author Matthew George
 * @version 1.3.6
 * 
 * @license Copyright 2009-2010 Matthew George
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
//var lastPublicRelease = [7, 13, 2010];
//var hoursSpentCodingThisVersion = 36; //estimated

/** @description Controls whether console logging is enabled or not.
 *	@type {boolean}
 *	@const
 *	@private
 */
var logging = false;	//set to false before releasing a new version
						//should also remove logging statements before releasing

/** @description The archives that appear in the context menus.
 *	@type {Array.<Array.<string>>}
 *	@const
 *	@private
 */
var contextArchives = [['Internet Archive', 'internetarchive'],
						['Google Cache', 'google'],
						['CoralCDN', 'coralcdn']];
						
/** @description Contains the search templates for each archive.
 *	@type {Array.<string, string>}
 *	@private
 */
var searchExp;

/** @description Contains error descriptions for the alerts.
 *	@type {Array.<string, string>}
 *	@private
 */
var alertErrorDesc;

/* ARCHIVE SEARCH FUNCTIONS */
/** @description Checks to see if a string is a valid url.
 *	@param {string} url The string to be tested.
 *	@return {Array.<(string|number)>} The url or error information.
 */
function parseUrl(url) {
	var urlArray; //declare return variable
	
	// check the url given; if undefined or url given is not
	// an http protocol address, return error
	if(url) {
		//url parameter is defined;
		//check to see if url is a valid search url
		
		//create regular expression variable to test string
		//Begin magic
		var urlCheck = /^(http:\/\/)?((?:[a-z0-9\-]*?(?:\.[a-z0-9\-]*?)*?\.[a-z][a-z0-9\-]*?[a-z])|(?:(?:(?:(?:[0-1]?[0-9]{1,2})|(?:2(?:[0-4][0-9])|(?:5[0-5])))(?:\.(?:(?:[0-1]?[0-9]{1,2})|(?:2(?:[0-4][0-9])|(?:5[0-5])))){3})|(?:\[[a-z0-9]{0,4}(?::[a-z0-9]{0,4}){5,7}\])))((?::[0-9]+)?(?:\/[^\s/;?#]+)*(?:\/(?:[^\s/;?#]+\.[a-z0-9]+)?)?(?:\?[^\s/;?#]*)?(?:#[^\s/;?#]*)?)$/i;
		//End magic
		
		if(urlCheck.test(url)) {
			//string is valid http address
			//parse output to create return array
			
			//apply regular expression and store output array to temp variable
			var tempArray = urlCheck.exec(url);
			
			//create empty return variable
			urlArray = new Array(3);
			
			//test if 'http://' declaration was included
			//if not, add it to full url string 
			//then store full url string to index 0 in return array
			//All urls *must* have the 'http://' declaration
			//I don't care if Chrome doesn't show it anymore
			if(tempArray[1] == undefined) {
				urlArray[0] = 'http://' + tempArray[0];
			}
			else {
				urlArray[0] = tempArray[0];
			}
			
			//store hostname string to index 1 in return array
			urlArray[1] = tempArray[2];
			
			//store everything after hostname to index 2 in return array
			//if there is nothing after hostname, store an empty string
			if(tempArray.length >= 4) {
				urlArray[2] = tempArray[3];
			}
			else {
				urlArray[2] = '';
			}
		}
		else {
			//string is not a valid http address
			//return error code 2
			urlArray = [-1, 2];
		}
	}
	else {
		// url parameter is undefined;
		// return error code 1
		urlArray = [-1, 1];
	}
	
	return urlArray;
}

/** @description Opens a new tab and searches for the given url using the given archive.
 *	@param {Array.<string>} url The url to be searched for.
 *	@param {string} archive The archive to search.
 */
function searchArchive(url, archive) {
	var searchurl; //this will contain the full search url
	var checks = [/<fullurl>/i, /<hostname>/i, /<path>/i]; //this is the regular expression array
															//used to check for placeholders
	
	// Obtain the search string template for the chosen archive
	if(searchExp[archive]) {
		searchurl = searchExp[archive];
	}
	
	// Replace the placeholders with the appropriate values
	for(var i in checks) {
		searchurl = searchurl.replace(checks[i], url[i]);
	}
	
	// If searchurl is defined, create a new tab using the url string stored in searchurl
	if(searchurl) {
		chrome.tabs.create({url: searchurl});
	}
}

/* ALERT FUNCTIONS */
/** @description Performs an http request on a given url string and checks to see if an error is returned or not.
 *	@param {string} taburl The url of the tab being checked.
 *	@param {number} tabid The id of the tab being checked.
 *	@private
 */
function checkForError(taburl, tabid) {
	// define a new http request, assign to variable xhr
	var xhr = new XMLHttpRequest();
	
	// set request to get header info from the given url
	xhr.open("HEAD", taburl, true);
	
	// define function called when the request's ready state changes
	xhr.onreadystatechange = function() {
		if (xhr.readyState == 4) {
			// HTTP response has been received
			// Check for errors and respond accordingly
			switch(xhr.status) {
				case 404:
					// Page not found
					displayAlert(tabid, taburl, 404);
					break;
					
				default:
					// No error caught - do nothing
			}
		}
	}
	
	// send the request
	xhr.send();
}

/** @description Displays an alert notification.
 *	@param {number} tabid The id of the tab that the error came from.
 *	@param {string} taburl The url of the tab that the error came from.
 *	@param {number} errorcode The http status code returned by the tab.
 *	@private
 */
function displayAlert(tabid, taburl, errorcode) {
	// Display alert notification if supported and permission given
	if(webkitNotifications) {
		// Create url string for alert that includes error and tab info
		var notifyUrl = 'alert.html?tabid='+tabid+'&taburl='+taburl+'&errorcode='+errorcode;
		// Create the notification
		var notification = webkitNotifications.createHTMLNotification(notifyUrl);
		// Show the notification
		notification.show();
	}
}

/** @description Returns the description of given error.
 *	@param {string} errorcode The error code.
 *	@return {string} The description of the error.
 */
function getErrorDesc(errorCode) {
	return alertErrorDesc[errorCode];
}

/** @description Returns the archives to display for a given error.
 *	@param {string} errorcode The error code.
 *	@return {Array.<string, string>} The archives to display.
 */
function getAlertArchives(errorCode) {
	var archiveNames;
	var archiveKeys;
	
	// store the correct archives for each error to variables to be returned
	// in version 2.x, this will pull from user settings in local storage
	switch(errorCode) {
		case '404':
			archiveKeys = ["internetarchive", "google", "coralcdn"];
			archiveNames = ["Internet Archive", "Google Cache", "CoralCDN"];
			break;
		
		default: 
			//if you have ended up here, you have done something wrong
			archiveKeys = ["internetarchive", "google", "coralcdn"];
			archiveNames = ["Internet Archive", "Google Cache", "CoralCDN"];
	}
	
	// return variables in an array
	return [archiveKeys, archiveNames];
}

/** @description Change the currently selected tab.
 *	@param {number} tabId The id of the tab to switch to.
 *	@private
 */
function changeSelectedTab(tabId) {
	//set the tab with the given tab id as the displayed tab
	chrome.tabs.update(parseInt(tabId), {selected: true});
}

/* CONTEXT MENU FUNCTIONS */
// where did all this come from???
/** @description Get the necessary url and search for it in the selected archive.
 *	@param {Object} info The info about the context item that was clicked.
 *	@param {Object} tab The info about the current tab.
 *	@param {string} archive The archive selected.
 *	@private
 */
function contextItemClicked(info, tab, archive){
	var urlArray;
	
	if(info.linkUrl) {
		urlArray = parseUrl(info.linkUrl);
	}
	else {
		urlArray = parseUrl(info.pageUrl);
	}
	
	if(urlArray.length >= 3) {
		searchArchive(urlArray, archive);
	}
}

/** @description Create a context menu group.
 *	@param {string} folderTitle The title of the group folder in the menu.
 *	@param {Array.<string>} folderContexts The contexts in which to display the group.
 *	@param {?Array.<string>} folderTargetUrls The target urls for the group.
 *	@param {?Array.<string>} folderDocUrls The doc urls for the group.
 *	@private
 */
function createContextGroup(folderTitle, folderContexts, folderTargetUrls, folderDocUrls) {
	var folderSettings = {
		title: folderTitle,
		contexts: folderContexts,
		targetUrlPatterns: folderTargetUrls,
		documentUrlPatterns: folderDocUrls};
		
	var folderId = chrome.contextMenus.create(folderSettings, function(){
		addContextArchive(folderId, folderContexts, 0);
	});
}

/** @description Adds an archive to the given folder.
 *	@param {number} folderId The id of the folder to add to.
 *	@param {Array.<string>} folderContexts The contexts in which to display the archive.
 *	@param {number} archiveId The id of the archive to add.
 *	@private
 */
function addContextArchive(folderId, folderContexts, archiveId) {
	var itemSettings = {
		title: contextArchives[archiveId][0],
		contexts: folderContexts,
		onclick: function(info, tab) {
			contextItemClicked(info, tab, contextArchives[archiveId][1]);
		},
		parentId: folderId
	};
	
	chrome.contextMenus.create(itemSettings, function(){
		if((archiveId + 1) < contextArchives.length) {
			addContextArchive(folderId, folderContexts, archiveId+1);
		}
	});
}

/* OPTIONS FUNCTIONS */
//All of these functions are ripped almost directly from a tutorial somewhere
//I am aware that this method of logging sucks, and that wrapping it in a function
//prevents you from seeing the actual line the message came from.
//I am too lazy to rewrite it or get rid of it.
//This function used for testing purposes only
/** @description Writes to the console.
 */
function log(txt) {
	//if logging is enabled, write given string to console
	if(logging) {
		console.log(txt);
	}
}

/** @description Gets a value from local storage.
 *	@param {*} key The key that the value is stored under.
 *	@return {*} The obtained value.
 */
function getLocalVar(key) {
	var value;
	log('Getting value for: ' + key);
	
	//attempt to get value pair for given key
	try {
		value = window.localStorage.getItem(key);
	} catch(e) {
		//attempt failed
		log("Error in getLocalVar() for key: " + key);
		log(e);
		value = null;
	}
	if(value) {
		//value is defined; attempt was successful
		log("Obtained value: " + value);
	}
	return value;
}

/** @description Stores a value in local storage.
 *	@param {*} key The key the value is to be stored under.
 *	@param {*} value The value to be stored.
 */
function setLocalVar(key, value) {
	//attempt to set the given key-value pair
	try {
		log("Setting the following key-value pair: " + key + ":" + value);
		window.localStorage.removeItem(key);
		window.localStorage.setItem(key, value);
	} catch(e) {
		//attempt failed
		log("Error in setLocalVar()");
		log(e);
	}
}

//This function does nothing that the user cares about for now.
//In version 2.x, users will hate this function
/** @description Check if the extension has been updated and respond accordingly.
 *	@private
 */
function checkIfUpdated() {
	//if stored version number is not equal to current version number, update is assumed
	//I used the correct version number this time :)
	if(!getLocalVar('waybackVersion') || getLocalVar('waybackVersion') != '1.3.6') {
		//stored version != current version
		//update the stored version number
		setLocalVar('waybackVersion', '1.3.6');
	}
}

/* EVENT LISTENERS */
/** @description Check for errors if necessary when a tab is updated
 *	@param {function(number, Object, Object)}
 */
chrome.tabs.onUpdated.addListener(function(tabid, changeInfo, updtdTab) {
	if(changeInfo.status == 'loading' && changeInfo.url != null) {
		var urlCheck = /^http:\/\//i; //no checking Chrome's built-in pages
		if(urlCheck.test(changeInfo.url)){
			checkForError(changeInfo.url, tabid);
		}
	}
});

/* EXTENSION INTITIALIZATION */
/** @description Initilizes the extension.
 *	@private
 */
function init() {
	//load search archives
	var archiveInfo = [['internetarchive', 'http://web.archive.org/web/*/<fullurl>', 'Internet Archive'],
				['google', 'http://www.google.com/search?q=cache:<fullurl>', 'Google Cache'],
				['coralcdn', 'http://<hostname>.nyud.net<path>', 'CoralCDN']];
	
	searchExp = new Array();
	
	for(var i in archiveInfo) {
		searchExp[archiveInfo[i][0]] = archiveInfo[i][1];
	}
	
	//I'm too lazy to finish rewriting the archive-handling code.
	//The extension still runs perfectly fine as is.
	
	//load alert error descriptions
	alertErrorDesc = new Array();
	alertErrorDesc['404'] = 'PAGE NOT FOUND';
	
	//setup context menu folders
	createContextGroup('Find link in archive', ['link'], ['http://*/*'], undefined);
	createContextGroup('Find page in archive', ['page'], undefined, ['http://*/*']);
	
	//perform update check on extension load
	checkIfUpdated();
}

init();